﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace NetFrameworkExtensions.Reflection
{
  public class ExpressionPrinter : ExpressionVisitor
  {
    int indentLevel = 0;
    string Indentation
    {
      get
      {
        return Enumerable.Range(1, indentLevel).Aggregate("", (a, i) => a + "  ");
      }
    }
    StringBuilder content = new StringBuilder();

    public override string ToString()
    {
      return content.ToString();
    }

    protected override Expression VisitBinary(BinaryExpression node)
    {
      content.Append(Indentation).Append("Binary ").Append(node.NodeType).AppendLine();
      using (Indent())
      {
        if (node.Left != null)
        {
          content.Append(Indentation).AppendLine("Left");
          using (Indent())
            Visit(node.Left);
        }
        if (node.Right != null)
        {
          content.Append(Indentation).AppendLine("Right");
          using (Indent())
            Visit(node.Right);
        }
        if (node.Conversion != null)
        {
          content.Append(Indentation).AppendLine("Conversion");
          using (Indent())
            Visit(node.Conversion);
        }
      }
      return node;
    }

    protected override Expression VisitBlock(BlockExpression node)
    {
      content.Append(Indentation).AppendLine("Block");
      using (Indent())
      {
        if (node.Variables != null && node.Variables.Count > 0)
        {
          content.Append(Indentation).AppendLine("Variables");
          using (Indent())
            foreach (var item in node.Variables)
              Visit(item);
        }
        if (node.Expressions != null && node.Expressions.Count > 0)
        {
          content.Append(Indentation).AppendLine("Expressions");
          using (Indent())
            foreach (var item in node.Expressions)
              Visit(item);
        }
        if (node.Result != null)
        {
          content.Append(Indentation).AppendLine("Result");
          using (Indent())
            Visit(node.Result);
        }
      }
      return node;
    }

    protected override CatchBlock VisitCatchBlock(CatchBlock node)
    {
      content.Append(Indentation).AppendLine("Catch");
      using (Indent())
      {
        if (node.Variable != null)
        {
          content.Append(Indentation).AppendLine("Variable");
          using (Indent())
            Visit(node.Variable);
        }
        if (node.Filter != null)
        {
          content.Append(Indentation).AppendLine("Filter");
          using (Indent())
            Visit(node.Filter);
        }
        if (node.Body != null)
        {
          content.Append(Indentation).AppendLine("Body");
          using (Indent())
            Visit(node.Body);
        }
      }
      return node;
    }

    protected override Expression VisitConditional(ConditionalExpression node)
    {
      content.Append(Indentation).AppendLine("Conditional");
      using (Indent())
      {
        if (node.Test != null)
        {
          content.Append(Indentation).AppendLine("Test");
          using (Indent())
            Visit(node.Test);
        }
        if (node.IfTrue != null)
        {
          content.Append(Indentation).AppendLine("IfTrue");
          using (Indent())
            Visit(node.IfTrue);
        }
        if (node.IfFalse != null)
        {
          content.Append(Indentation).AppendLine("IfFalse");
          using (Indent())
            Visit(node.IfFalse);
        }
      }
      return node;
    }

    protected override Expression VisitConstant(ConstantExpression node)
    {
      content.Append(Indentation).Append("Constant ").Append(node.Value == null ? "null" : node.Value.ToString()).AppendFormat(" [{0}]", node.Type.Name).AppendLine();
      return node;
    }

    protected override Expression VisitDynamic(DynamicExpression node)
    {
      content.Append(Indentation).AppendLine("Dynamic");
      return node;
    }

    protected override Expression VisitDefault(DefaultExpression node)
    {
      content.Append(Indentation).AppendFormat("Default({0})", node.Type.Name).AppendLine();
      return node;
    }

    protected override Expression VisitIndex(IndexExpression node)
    {
      content.Append(Indentation).AppendLine("Indexer");
      using (Indent())
      {
        if (node.Object != null)
        {
          content.Append(Indentation).AppendLine("Object");
          using (Indent())
            Visit(node.Object);
        }
        if (node.Arguments != null && node.Arguments.Count > 0)
        {
          content.Append(Indentation).AppendLine("Arguments");
          using (Indent())
            foreach (var item in node.Arguments)
              Visit(item);
        }
      }
      return node;
    }

    protected override Expression VisitInvocation(InvocationExpression node)
    {
      content.Append(Indentation).AppendLine("Invocation");
      using (Indent())
      {
        if (node.Expression != null)
        {
          content.Append(Indentation).AppendLine("Expression");
          using (Indent())
            Visit(node.Expression);
        }
        if (node.Arguments != null && node.Arguments.Count > 0)
        {
          content.Append(Indentation).AppendLine("Arguments");
          using (Indent())
            foreach (var item in node.Arguments)
              Visit(item);
        }
      }
      return node;
    }

    protected override Expression VisitLabel(LabelExpression node)
    {
      content.Append(Indentation).Append("Label ").AppendFormat("{0} : {1}", node.Target.Name, node.Target.Type.Name).AppendLine();
      using (Indent())
      {
        if (node.DefaultValue != null)
        {
          content.Append(Indentation).AppendLine("DefaultValue");
          using (Indent())
            Visit(node.DefaultValue);
        }
      }
      return node;
    }

    protected override Expression VisitLambda<T>(Expression<T> node)
    {
      content.Append(Indentation).AppendFormat("Lambda \"{0}\"", node.Name).AppendLine();
      using (Indent())
      {
        if (node.Parameters != null && node.Parameters.Count > 0)
        {
          content.Append(Indentation).AppendLine("Parameters");
          using (Indent())
            foreach (var item in node.Parameters)
              Visit(item);
        }
        if (node.Body != null)
        {
          content.Append(Indentation).AppendLine("Body");
          using (Indent())
            Visit(node.Body);
        }
      }
      return node;
    }

    protected override Expression VisitDebugInfo(DebugInfoExpression node)
    {
      content.Append(Indentation).AppendLine("DebugInfo");
      return node;
    }

    protected override ElementInit VisitElementInit(ElementInit node)
    {
      content.Append(Indentation).AppendLine("ElementInit");
      content.Append(Indentation).AppendFormat("ElementInit \"{0}\"", node.AddMethod);
      using (Indent())
      {
        if (node.Arguments != null && node.Arguments.Count > 0)
        {
          content.Append(Indentation).AppendLine("Arguments");
          using (Indent())
            foreach (var item in node.Arguments)
              Visit(item);
        }
      }
      return node;
    }

    protected override Expression VisitGoto(GotoExpression node)
    {
      content.Append(Indentation).AppendLine("Goto");
      return node;
    }

    protected override LabelTarget VisitLabelTarget(LabelTarget node)
    {
      content.Append(Indentation).AppendLine("LabelTarget");
      return node;
    }

    protected override Expression VisitListInit(ListInitExpression node)
    {
      content.Append(Indentation).AppendLine("ListInit");
      return node;
    }

    protected override Expression VisitLoop(LoopExpression node)
    {
      content.Append(Indentation).AppendLine("Loop");
      return node;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
      content.Append(Indentation).AppendFormat(@"Member ""{0}.{1}"" {2}", node.Member.DeclaringType.Name, node.Member.Name, node.Member).AppendLine();
      using (Indent())
      {
        if (node.Expression != null)
        {
          content.Append(Indentation).AppendLine("Expression");
          using (Indent())
            Visit(node.Expression);
        }
      }
      return node;
    }

    protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
    {
      content.Append(Indentation).AppendFormat(@"MemberAssignment [{0}] on ""{1}.{2}"" {3}", node.BindingType, node.Member.DeclaringType.Name, node.Member.Name, node.Member).AppendLine();
      using (Indent())
      {
        if (node.Expression != null)
        {
          content.Append(Indentation).AppendLine("Expression");
          using (Indent())
            Visit(node.Expression);
        }
      }
      return node;
    }

    protected override MemberBinding VisitMemberBinding(MemberBinding node)
    {
      return base.VisitMemberBinding(node);
    }

    protected override Expression VisitMemberInit(MemberInitExpression node)
    {
      content.Append(Indentation).AppendLine("MemberInit");
      using (Indent())
      {
        if (node.NewExpression != null)
        {
          content.Append(Indentation).AppendLine("NewExpression");
          using (Indent())
            Visit(node.NewExpression);
        }
        if (node.Bindings != null && node.Bindings.Count > 0)
        {
          content.Append(Indentation).AppendLine("Bindings");
          using (Indent())
            foreach (var item in node.Bindings)
              VisitMemberBinding(item);
        }
      }
      return node;
    }

    protected override MemberListBinding VisitMemberListBinding(MemberListBinding node)
    {
      content.Append(Indentation).AppendLine("MemberListBinding");
      content.Append(Indentation).AppendFormat(@"MemberListBinding [{0}] on ""{1}.{2}"" {3}", node.BindingType, node.Member.DeclaringType.Name, node.Member.Name, node.Member).AppendLine();
      using (Indent())
      {
        if (node.Initializers != null)
        {
          content.Append(Indentation).AppendLine("Initializers");
          using (Indent())
            foreach (var item in node.Initializers)
              VisitElementInit(item);
        }
      }
      return node;
    }

    protected override MemberMemberBinding VisitMemberMemberBinding(MemberMemberBinding node)
    {
      content.Append(Indentation).AppendFormat(@"MemberMemberBinding [{0}] on ""{1}.{2}"" {3}", node.BindingType, node.Member.DeclaringType.Name, node.Member.Name, node.Member).AppendLine();
      using (Indent())
      {
        if (node.Bindings != null)
        {
          content.Append(Indentation).AppendLine("Bindings");
          using (Indent())
            foreach (var item in node.Bindings)
              VisitMemberBinding(item);
        }
      }
      return node;
    }

    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
      content.Append(Indentation).AppendFormat(@"MethodCall : {0}""{1}.{2}"" {3}", node.Method.IsStatic ? "static " : "", node.Method.DeclaringType.Name, node.Method.Name, node.Method).AppendLine();
      using (Indent())
      {
        if (node.Object != null)
        {
          content.Append(Indentation).AppendLine("Object");
          using (Indent())
            Visit(node.Object);
        }
        if (node.Arguments != null)
        {
          content.Append(Indentation).AppendLine("Arguments");
          using (Indent())
            foreach (var item in node.Arguments)
              Visit(item);
        }
      }
      return node;
    }

    protected override Expression VisitNew(NewExpression node)
    {
      content.Append(Indentation).AppendFormat("New \"{0}\" with {1}", node.Type.Name, node.Constructor == null ? "no constructor" : "constructor " + node.Constructor).AppendLine();
      using (Indent())
      {
        if (node.Arguments != null && node.Arguments.Count > 0)
        {
          content.Append(Indentation).AppendLine("Arguments");
          using (Indent())
            foreach (var item in node.Arguments)
              Visit(item);
        }
        if (node.Members != null && node.Members.Count > 0)
        {
          content.Append(Indentation).AppendLine("Members");
          using (Indent())
            foreach (var item in node.Members)
              content.Append(Indentation).Append(item).AppendLine();
        }
      }
      return node;
    }

    protected override Expression VisitNewArray(NewArrayExpression node)
    {
      content.Append(Indentation).AppendFormat("NewArray of {0}", node.Type.FullName).AppendLine();
      using (Indent())
      {
        if (node.Expressions != null && node.Expressions.Count > 0)
        {
          content.Append(Indentation).AppendLine("Expressions");
          using (Indent())
            foreach (var item in node.Expressions)
              Visit(item);
        }
      }
      return node;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
      content.Append(Indentation).AppendFormat("Parameter \"{0}\" : {1}", node.Name, node.Type.FullName).AppendLine();
      return node;
    }

    protected override Expression VisitRuntimeVariables(RuntimeVariablesExpression node)
    {
      content.Append(Indentation).AppendFormat("RuntimeVariables : {0}", node.Type.FullName).AppendLine();
      using (Indent())
      {
        if (node.Variables != null && node.Variables.Count > 0)
        {
          content.Append(Indentation).AppendLine("Variables");
          using (Indent())
            foreach (var item in node.Variables)
              Visit(item);
        }
      }
      return node;
    }

    protected override Expression VisitSwitch(SwitchExpression node)
    {
      content.Append(Indentation).AppendFormat("Switch by {0}", node.Comparison);
      using (Indent())
      {
        if (node.SwitchValue != null)
        {
          content.Append(Indentation).AppendLine("SwitchValue");
          using (Indent())
            Visit(node.SwitchValue);
        }
        if (node.Cases != null && node.Cases.Count > 0)
        {
          content.Append(Indentation).AppendLine("Cases");
          using (Indent())
            foreach (var item in node.Cases)
              VisitSwitchCase(item);
        }
        if (node.DefaultBody != null)
        {
          content.Append(Indentation).AppendLine("DefaultBody");
          using (Indent())
            Visit(node.DefaultBody);
        }
      }
      return node;
    }

    protected override SwitchCase VisitSwitchCase(SwitchCase node)
    {
      content.Append(Indentation).AppendFormat("SwitchCase").AppendLine();
      using (Indent())
      {
        if (node.TestValues != null && node.TestValues.Count > 0)
        {
          content.Append(Indentation).AppendLine("TestValues");
          using (Indent())
            foreach (var item in node.TestValues)
              Visit(item);
        }
        if (node.Body != null)
        {
          content.Append(Indentation).AppendLine("Body");
          using (Indent())
            Visit(node.Body);
        }
      }
      return node;
    }

    protected override Expression VisitTry(TryExpression node)
    {
      content.Append(Indentation).AppendLine("Try");
      using (Indent())
      {
        if (node.Body != null)
        {
          content.Append(Indentation).AppendLine("Body");
          using (Indent())
            Visit(node.Body);
        }
        if (node.Fault != null)
        {
          content.Append(Indentation).AppendLine("Fault");
          using (Indent())
            Visit(node.Fault);
        }
        if (node.Finally != null)
        {
          content.Append(Indentation).AppendLine("Finally");
          using (Indent())
            Visit(node.Finally);
        }
        if (node.Handlers != null && node.Handlers.Count > 0)
        {
          content.Append(Indentation).AppendLine("Handlers");
          using (Indent())
            foreach (var item in node.Handlers)
              VisitCatchBlock(item);
        }
      }
      return node;
    }

    protected override Expression VisitTypeBinary(TypeBinaryExpression node)
    {
      content.Append(Indentation).AppendFormat("TypeBinary : {0}", node.TypeOperand.FullName);
      using (Indent())
      {
        if (node.Expression != null)
        {
          content.Append(Indentation).AppendLine("Expression");
          using (Indent())
            Visit(node.Expression);
        }
      }
      return node;
    }

    protected override Expression VisitUnary(UnaryExpression node)
    {
      content.Append(Indentation).AppendFormat("Unary : {0} ({1})", node.NodeType, node.Method).AppendLine();
      using (Indent())
      {
        if (node.Operand != null)
        {
          content.Append(Indentation).AppendLine("Operand");
          using (Indent())
            Visit(node.Operand);
        }
      }
      return node;
    }

    protected override Expression VisitExtension(Expression node)
    {
      content.Append(Indentation).AppendLine("Extension");
      return base.VisitExtension(node);
    }

    private IDisposable Indent()
    {
      return new GenericMonitor(() => indentLevel++, () => indentLevel--);
    }
  }

  public static class ExpressionPrinterExtensions
  {
    public static string Print(this Expression expression)
    {
      if (expression == null) throw new ArgumentNullException("expression");

      var printer = new ExpressionPrinter();
      printer.Visit(expression);
      return printer.ToString();
    }
  }
}
